@filigran/chatbot 3.19.1 → 3.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,122 @@ the versions follow [semantic versioning](https://semver.org/spec/v2.0.0.html).
8
8
  This file starts at 3.10.0; earlier releases are on the
9
9
  [releases page](https://github.com/FiligranHQ/filigran-ui/releases).
10
10
 
11
- ## 3.19.1 — unreleased
11
+ ## 3.21.0 — unreleased
12
+
13
+ ### Added
14
+
15
+ - `onConversationChange` says why the conversation changed: its second argument
16
+ is `{ created }`, true for a conversation the panel has just created from a
17
+ new chat (its first message or file), false for one opened from the list, on
18
+ a reload or in place of a deleted one, and when it is left for a new chat. A
19
+ host could not tell an opened conversation from a created one, as both came
20
+ after a new chat.
21
+
22
+ ## 3.20.0 — 2026-11-10
23
+
24
+ ### Added
25
+
26
+ - `ChatMarkerRenderer.standalone`: an answer that is the renderer's marker and
27
+ nothing else shows the card alone, without the agent's name, the time or the
28
+ actions, as a note between messages (a change of agent, for instance). Each
29
+ answer showed its marker's card inside the answer's frame.
30
+ - The panel speaks German, Italian, Korean, Russian and Simplified Chinese on
31
+ its own: a catalog for each, picked from `locale` by its language whatever
32
+ the region (`de-de`, `it-it`, `ko-kr`, `ru-ru`, `zh-cn`). Those languages
33
+ showed in English unless the host translated every key through `t`.
34
+ Traditional Chinese tags (`zh-TW`, `zh-HK`, `zh-Hant`) keep falling back.
35
+ - An agent the backend marks `disable_chat: true` (`XtmAgent.disableChat`) is
36
+ said `Not recommended for chat` in the agent picker, and the composer warns
37
+ while it is the conversation's agent as the header names it. It stays
38
+ selectable.
39
+ - The header names the channel a conversation was started in under the
40
+ agent's name (`Agent · {channel}`), as its row does, when the host draws no
41
+ `headerSubtitle`; `channelLabel` names it there too. The restore's
42
+ `channel` names it before the list is read.
43
+ - The composer's warning also covers an agent the draft `@`-mentions that is
44
+ not meant for chat, until the mention is removed.
45
+ - With workspaces, the list's `Not in a workspace` group has a `New
46
+ conversation outside a workspace` button, beside each workspace's own.
47
+ - In a reply's "Working files" list, a file the same reply wrote again later,
48
+ with the same name and kind of file (MIME type without its parameters, else
49
+ the type label), carries a "Superseded" tag; its last version carries none.
50
+ A file listed again ranks where it first appeared.
51
+ - `ChatPanelHandle.attachFiles(files)` puts files in the composer's tray as if
52
+ the user picked them, uploaded and held to the same limits, for the next
53
+ message: files another app shared to the host. Nothing happens where the
54
+ panel takes no files.
55
+ - `initialConversationListCollapsed` starts the conversation list collapsed to
56
+ its strip, from which the user opens it: for a phone or a narrow window.
57
+ Read once, when the panel mounts.
58
+ - `replaceMessage(message, { streaming })` lets a host draw a message its own
59
+ way: `undefined` or a boolean leaves it to the panel, `null` leaves it out,
60
+ and anything else is drawn in its place without the message's frame. It is
61
+ asked of every message, the reply's empty placeholder included, and a throw
62
+ leaves that message to the panel. For messages a host folds into one of its
63
+ own, such as the replies of a turn cut and resumed shown as one card.
64
+ - `expandedWorkspaceGroups` and `onExpandedWorkspaceGroupsChange` let a host
65
+ keep, per user, which workspace groups of the conversation list are open.
66
+ Given, every other group starts collapsed until the user opens it, and a
67
+ workspace the user creates from the list opens. A host still reading them
68
+ passes `undefined` until it has them. Unset, groups start open and the ones
69
+ the user collapses are remembered per browser, as before.
70
+
71
+ ### Changed
72
+
73
+ - The agent picker follows the conversation on screen: opening a conversation
74
+ of another agent, or reading back one the host continued with another agent,
75
+ picks that agent, so a new chat starts with it, after a reload too, and
76
+ `onAgentChange` is called.
77
+ An agent picked in the menu meanwhile stays picked. The picker kept the agent
78
+ last picked in the menu, while the header named the conversation's.
79
+
80
+ ### Fixed
81
+
82
+ - `maxTotalSize: Infinity` lifts the cap on the total size of a message's
83
+ files, as a host with a per-file limit only passes it (XTM One's web chat and
84
+ Office add-in). The panel read a value that is not finite as invalid and kept
85
+ its 50 MB default total.
86
+ - A message sent from a new chat no longer comes back as the next new chat's
87
+ draft: the draft leaves the conversation it was typed in as it is sent. The
88
+ write of the emptied composer could land on the conversation the send
89
+ created, leaving the sent text under the new chat's draft.
90
+ - An image's lightbox is named after the image (its alt text, or the file's
91
+ name for an uploaded picture) and keeps `Image preview` for one with none; it
92
+ read `Image preview` for every picture.
93
+ - Tab and Shift+Tab stay on the lightbox's own controls (Download when offered,
94
+ Close) while it is open; they reached the page under it.
95
+ - An image's Expand button is visible while it has keyboard focus, so the focus
96
+ given back after the lightbox closes can be seen. It showed on hover only.
97
+ - A refusal whose body has no `detail` shows its `error` when that is a
98
+ sentence, as the products' proxies answer (`{ "error": "..." }`, beside a
99
+ `status` or not), never a code (`invalid_token`, `Unauthorized`) nor a
100
+ status's reason phrase (`Not Found`, `Too Many Requests`). It shows above
101
+ the composer, in place of the answer, on a failed upload's chip, under the
102
+ conversation list and on a conversation that could not be loaded. The panel
103
+ showed its generic message instead.
104
+ - The status line has its own wording when it has no agent's name (`The agent
105
+ is answering…`, `Consulting an agent…`, `Transferring to another agent…`),
106
+ where it filled the name with a lowercase `the agent`: `the agent is
107
+ answering…` started without a capital. A host translating through `t` in a
108
+ language the panel has no catalog for translates these three keys; it
109
+ translated `the agent` before, and shows them in English until it does.
110
+ - A host that can steer (a `steer` route, on by default) gets its conversation
111
+ created before the first message, so the first turn can be steered. Where
112
+ turns end with their stream (no `cancel`, `skipWait` or `events` route) the
113
+ panel learned the conversation's id only at the turn's end, and a message
114
+ typed while that first turn ran could not be sent.
115
+ - A steer the backend refuses because the turn just ended (409) goes out as
116
+ the next message, as a send during a turn winding down does, whatever
117
+ `holdSendsDuringTurn` says. Without it, the text went back to the composer
118
+ and the user had to send it again.
119
+ - Files added from outside the panel's own events (a host's call) upload too:
120
+ the upload launched on a timer that assumed the tray had been updated
121
+ already, which only a pick guaranteed.
122
+ - A wait a resumed turn continued (`wait_start`) ends on `wait_continued`
123
+ with `Analyzing results…`, as the turn moves on to read what it waited for.
124
+ The status line read `Thinking...` until the next status.
125
+
126
+ ## 3.19.1 — 2026-11-02
12
127
 
13
128
  ### Added
14
129
 
@@ -17,9 +132,18 @@ This file starts at 3.10.0; earlier releases are on the
17
132
  the region (`de-de`, `it-it`, `ko-kr`, `ru-ru`, `zh-cn`). Those languages
18
133
  showed in English unless the host translated every key through `t`.
19
134
  Traditional Chinese tags (`zh-TW`, `zh-HK`, `zh-Hant`) keep falling back.
135
+ - Inline, as a sidebar and fullscreen, the composer's footer has a line of
136
+ its own for what the keys do (`filigran-chat-composer-hint`), above the
137
+ disclaimer: an idle composer reads
138
+ `Enter to send · Shift+Enter for new line`, a busy one the status it read
139
+ before, and the line keeps its height when empty. The floating window keeps
140
+ one line, where a status takes the disclaimer's place.
20
141
 
21
142
  ### Fixed
22
143
 
144
+ - `filigran-chat-disclaimer` holds the disclaimer alone. It held the
145
+ composer's status in its place while a turn ran or a file failed to
146
+ upload; the status is now `filigran-chat-composer-hint`.
23
147
  - A refusal whose body has no `detail` shows its `error` when that is a
24
148
  sentence, as the products' proxies answer (`{ "error": "..." }`, beside a
25
149
  `status` or not), never a code (`invalid_token`, `Unauthorized`) nor a
@@ -33,8 +157,14 @@ This file starts at 3.10.0; earlier releases are on the
33
157
  answering…` started without a capital. A host translating through `t` in a
34
158
  language the panel has no catalog for translates these three keys; it
35
159
  translated `the agent` before, and shows them in English until it does.
160
+ - A conversation list that could not be read (a refusal, a server or network
161
+ error) says "Could not load your conversations." with a Retry, in the
162
+ fullscreen list and the header's history menu, where it said "No
163
+ conversations yet". A read that fails once the list was read keeps the
164
+ conversations it showed, unless it was refused (401/403), which drops them;
165
+ a backend without the route (404/405) still shows an empty list.
36
166
 
37
- ## 3.19.0 — unreleased
167
+ ## 3.19.0 — 2026-11-02
38
168
 
39
169
  ### Added
40
170
 
@@ -190,7 +320,7 @@ This file starts at 3.10.0; earlier releases are on the
190
320
  query: the list stayed filtered by a search the user could neither see nor
191
321
  clear.
192
322
 
193
- ## 3.18.1 — unreleased
323
+ ## 3.18.1 — 2026-11-02
194
324
 
195
325
  ### Fixed
196
326
 
package/README.md CHANGED
@@ -105,9 +105,9 @@ import { ChatPanel } from '@filigran/chatbot';
105
105
  | `composerToolbar` | `React.ReactNode` | — | Extra controls appended to the composer toolbar. The escape hatch for host-specific affordances (XTM One's session-tool picker) — the package never learns what they are. Pass nothing and the toolbar simply has none. |
106
106
  | `storageKeyPrefix` | `string` | — | Namespaces every key the panel keeps in browser storage (`<prefix>:filigranChatConversationId`, …), for a host mounting more than one panel |
107
107
  | `conversationId` | `string \| null` | — | The conversation on screen when the host owns it (a `?c=` URL); `null` is a new chat. Left `undefined`, the panel remembers its own, and a host-owned one is never written to storage |
108
- | `onConversationChange` | `(id: string \| null) => void` | — | Called whenever the conversation on screen changes, whoever changed it (a first answer, New chat, the history) |
108
+ | `onConversationChange` | `(id: string \| null, change: { created: boolean }) => void` | — | Called whenever the conversation on screen changes, whoever changed it (a first answer, New chat, the history); `created` is true for one the panel has just created from a new chat (its first message or file) |
109
109
  | `initialAgent` | `string` | — | Agent selected once the list loads, by id or slug, over the remembered one |
110
- | `onAgentChange` | `(agent: XtmAgent) => void` | — | Called when another agent is picked |
110
+ | `onAgentChange` | `(agent: XtmAgent) => void` | — | Called when another agent is picked, in the menu or by opening a conversation of another agent: the picker follows the conversation on screen |
111
111
  | `readOnly` | `boolean` | `false` | Hides the composer: a conversation the user may read but not write to |
112
112
  | `readOnlyNotice` | `React.ReactNode` | — | Shown in place of the composer while `readOnly` |
113
113
  | `getRequestExtras` | `() => Record<string, unknown>` | — | Fields merged into every message body, read at send time (the host's own per-message options). They never override the panel's fields |
@@ -119,6 +119,7 @@ import { ChatPanel } from '@filigran/chatbot';
119
119
  | `onArtifactOpen` | `(artifact: ChatArtifact) => void` | — | Opens a deck, report or video in the host, in place of the `Open in XTM One` link |
120
120
  | `headerActions` / `banner` / `composerHeader` / `transcriptFooter` / `welcome` | `React.ReactNode` | — | The host's content in the header, above the thread, above the composer, after the last message, or in place of the welcome screen |
121
121
  | `renderMessageExtras` / `messageFooterActions` | `(message, { streaming }) => React.ReactNode` | — | The host's content under a message, and controls in a finished answer's footer; `streaming` while the message is the answer still being written. `message.raw` holds the backend's own fields — see [Host extensions](#host-extensions) |
122
+ | `replaceMessage` | `(message, { streaming }) => React.ReactNode` | — | The host's own drawing of a message: `undefined` or a boolean leaves it to the panel, `null` leaves it out, anything else is drawn in its place without the message's frame (name, time, actions). Asked of every message, the reply's empty placeholder included. A throw leaves that message to the panel. For messages a host folds into one of its own, such as a turn cut and resumed shown as one card |
122
123
  | `sidebarRowActions` | `(conversation) => React.ReactNode` | — | The host's controls on a row of the conversation list |
123
124
  | `sidebarHeaderActions` | `React.ReactNode` | — | The host's controls beside New conversation |
124
125
  | `sidebarSections` | `React.ReactNode \| ((search: ChatSidebarSearch) => React.ReactNode)` | — | Sections under the list; a function draws them from the list's search (`query`, trimmed), again when the search or the function changes (pass a stable one, and put hooks in a component it returns), and offers the search box whatever the number of conversations |
@@ -137,12 +138,14 @@ import { ChatPanel } from '@filigran/chatbot';
137
138
  | `onTurnRunningChange` | `(running: boolean, info?: ChatTurnEnd) => void` | — | A turn started or stopped running on screen, own or relayed: the state of the Stop button and of `draft.turnRunning`; with `false` comes how it ended, `info.ended`: `'answered'`, `'stopped'` or `'failed'` — see [Host extensions](#host-extensions). After a provisional end (see [Turns that outlive their stream](#turns-that-outlive-their-stream)) `false` comes at once, while sends are still held as the turn winds down: until its stored end or its stream's close, at most 5 s |
138
139
  | `onBeforeSend` | `(draft) => false \| { text?, displayText?, extras? } \| void` | — | Sees every non-empty send first and takes it over (`false`), rewords it, shows the user's bubble other words or adds body fields (a message with fields of its own is held with `holdSendsDuringTurn`) — see [Host extensions](#host-extensions) |
139
140
  | `hideConversationList` | `boolean` | `false` | Hides the conversation list of fullscreen and inline modes, for a rail or a task pane (usable from 320 px wide); the header's history menu stands in for it — see [Host extensions](#host-extensions) |
141
+ | `expandedWorkspaceGroups` | `string[]` | — | The workspace groups the user has opened (workspace ids), for a host that keeps them per user: every other group starts collapsed, and one the user creates from the list opens. Read once, the first time it is given (`undefined` while the host reads them). Unset, groups start open and the collapsed ones are remembered per browser |
142
+ | `onExpandedWorkspaceGroupsChange` | `(keys: string[]) => void` | — | The groups the user has opened, each time one is opened or collapsed (see `expandedWorkspaceGroups`) |
140
143
  | `holdSendsDuringTurn` | `boolean` | `false` | A message that cannot steer is held and sent when the turn ends, instead of waiting in the composer. A turn winding down after a provisional end (until its stored end or its stream's close, at most 5 s) holds sends with or without it |
141
144
  | `onBeforeMove` | `(conversation, workspace \| null) => boolean \| Promise<boolean>` | — | Asked before a drag or a row's Move menu files a conversation into a workspace (`null`: out of one); `false` leaves it where it was |
142
145
  | `onConversationActionFailed` | `(action, conversation \| null, error \| null) => boolean \| void` | — | A failed `'delete'`, `'rename'`, `'move'` or `'clearAll'`, with the backend's reason; return `true` when the host told the user itself, and the panel says nothing |
143
146
  | `sidebarSearchThreshold` | `number` | `7` | The conversation list's search box shows above this many conversations; `0` shows it from the first |
144
- | `channelLabel` | `(channel: string) => string \| null \| undefined` | — | Names the channel a row's conversation was started in (`Started in {channel}`); `null` shows nothing, `undefined` keeps the panel's names (Slack, Teams, WhatsApp, Telegram, Discord, Google Chat) |
145
- | `headerTitle` / `headerSubtitle` | `React.ReactNode \| ((context: ChatHeaderContext) => React.ReactNode)` | — | A title beside the header's agent name and a line under it, or functions of the conversation on screen (as the list holds it) and the agent |
147
+ | `channelLabel` | `(channel: string) => string \| null \| undefined` | — | Names the channel a conversation was started in, on its row (`Started in {channel}`) and in the header (`Agent · {channel}`); `null` shows nothing, `undefined` keeps the panel's names (Slack, Teams, WhatsApp, Telegram, Discord, Google Chat) |
148
+ | `headerTitle` / `headerSubtitle` | `React.ReactNode \| ((context: ChatHeaderContext) => React.ReactNode)` | — | A title beside the header's agent name and a line under it, or functions of the conversation on screen (as the list holds it) and the agent. Without `headerSubtitle`, the line reads `Agent · {channel}` for a conversation started in a channel its row names |
146
149
  | `agentHref` | `(agent: XtmAgent) => string \| undefined` | — | Links the header's agent name to this address (http(s) or a path of the host's origin), in place; the chevron beside it opens the picker |
147
150
  | `onAgentNameClick` | `(agent: XtmAgent) => void` | — | Takes a plain click on the header's agent name (over `agentHref`); the chevron beside it opens the picker |
148
151
  | `clearAllAction` | `'delete' \| 'archive'` | `'delete'` | What Clear all does, for its wording (`Archive all conversations`) |
@@ -194,13 +197,16 @@ A host adds what is its own without the package learning about it. An answer
194
197
  marker `[[KIND:args]]` renders the host's card where it stands, once the answer
195
198
  is done (`renderWhileStreaming` to show it earlier); a renderer named `FILE`,
196
199
  `PRESENTATION`, `REPORT` or `VIDEO` replaces the built-in card, and a marker no
197
- renderer names stays text:
200
+ renderer names stays text. A `standalone` renderer draws an answer that is its
201
+ marker and nothing else as the card alone, without the agent's name, the time
202
+ or the actions: a note between messages, such as a change of agent:
198
203
 
199
204
  ```tsx
200
205
  const markers = useMemo(
201
206
  () => ({
202
207
  WORK_ITEM: { render: (id) => <WorkItemCard id={id} /> },
203
208
  INVESTIGATION: { render: (id) => <InvestigationBlock id={id} />, fullWidth: true },
209
+ AGENT_SWITCH: { render: (id) => <AgentSwitchDivider id={id} />, fullWidth: true, standalone: true },
204
210
  }),
205
211
  [],
206
212
  );
@@ -465,6 +471,11 @@ Returns available AI agents.
465
471
  `badges` is optional: short labels, already translated, shown after the
466
472
  agent's name in the agent picker. Anything but strings is ignored.
467
473
 
474
+ `disable_chat` is optional: `true` marks an agent meant for assignments rather
475
+ than chat. The picker says `Not recommended for chat` after its name, and the
476
+ composer warns while it is the agent written to; it stays selectable. The
477
+ panel reads it as `XtmAgent.disableChat`.
478
+
468
479
  ### `POST {apiBaseUrl}/chat/sessions`
469
480
 
470
481
  Restores conversation history (and, implicitly, resolves the session).
@@ -508,6 +519,11 @@ without one shows no time. `feedback` is the caller's own rating of an
508
519
  assistant answer: the thumbs are then locked on it ("Feedback already
509
520
  submitted").
510
521
 
522
+ The response may also carry the conversation's `channel` (`slack`, `teams`,
523
+ …), as the list does: the header names it (`Agent · {channel}`) without
524
+ reading the list, which floating and sidebar modes read only once the history
525
+ menu opens.
526
+
511
527
  The response **should echo a `conversation_id`**. When the stored
512
528
  `conversation_id` no longer exists (e.g. the platform was reset but the
513
529
  browser kept an old id in `localStorage`), the backend is expected to
@@ -564,14 +580,15 @@ Lists the user's past conversations for the history menu in the chat header.
564
580
  ```
565
581
 
566
582
  `last_message_preview` and `channel` are optional: a row shows the preview
567
- under its title, and names a messaging channel the conversation was started
568
- in (see `channelLabel`).
583
+ under its title, and the search matches it; the row and the header name a
584
+ messaging channel the conversation was started in (see `channelLabel`).
569
585
 
570
586
  Selecting a conversation restores it through the existing
571
587
  `POST /chat/sessions` contract above. The endpoint is fetched lazily when the
572
588
  history menu opens; a backend that doesn't implement it yet (404/405) simply
573
589
  yields an empty list, so the menu shows its empty state instead of breaking
574
- the chat. Set `apiEndpoints.history` to `null` to hide the history menu
590
+ the chat. Any other failed read shows "Could not load your conversations."
591
+ with a Retry, and keeps the conversations a previous read gave. Set `apiEndpoints.history` to `null` to hide the history menu
575
592
  entirely, or point it at a dedicated path if your proxy can't route `GET` on
576
593
  the sessions path.
577
594
 
@@ -834,13 +851,15 @@ if the user already typed something new), the send resolves `'failed'` and the
834
851
  refusal's [reason](#a-refusals-reason), when there is one, shows above the composer — so a backend
835
852
  without steering support degrades gracefully and the message is never silently
836
853
  lost. A `409` means no turn is running any more, an ordinary race: no notice
837
- shows, and with `holdSendsDuringTurn` the message is held and sent as the next
838
- turn instead of going back to the composer. Set `apiEndpoints.steer` to `null`
839
- to disable the steering affordances entirely.
854
+ shows, and the message is held and sent as the next turn, with or without
855
+ `holdSendsDuringTurn`. Set `apiEndpoints.steer` to `null` to disable the
856
+ steering affordances entirely.
840
857
 
841
858
  Steering only applies to sends on the `rest` backend with a known
842
- `conversation_id` (the first turn of a fresh conversation only receives its id
843
- on `done`). A message with attachments steers only with `steerAttachments`
859
+ `conversation_id`. The stream names a fresh conversation only on `done`, so
860
+ while steering is on a new chat is created (`POST {sessions}`) before its first
861
+ message, and that first turn can be steered too; with `steer: null` it is not.
862
+ A message with attachments steers only with `steerAttachments`
844
863
  (once every file is uploaded); one carrying conversation references, agent
845
864
  mentions or body fields of its own (`send`'s `extras`, `onBeforeSend`'s
846
865
  `extras`) never steers, since a steer reaches the agent as text alone: it waits
package/dist/index.d.ts CHANGED
@@ -36,8 +36,11 @@ interface ApiEndpoints {
36
36
  * message was persisted and will be injected into the running agentic loop
37
37
  * at the next iteration boundary. On a non-2xx response the optimistic
38
38
  * bubble is rolled back and the text is restored into the composer, so a
39
- * backend without steering support degrades gracefully. Set to null to
40
- * disable mid-run steering entirely.
39
+ * backend without steering support degrades gracefully. A new chat is
40
+ * created (`POST {sessions}`) before its first message, so a steer can name
41
+ * the first turn's conversation: the stream names it only when the turn
42
+ * ends. Set to null to disable mid-run steering entirely, and that create
43
+ * with it.
41
44
  */
42
45
  steer?: string | null;
43
46
  /**
@@ -471,7 +474,7 @@ interface ChatPanelProps {
471
474
  onDownloadError?: (error: unknown, attachment: ChatAttachment) => void;
472
475
  /** Maximum number of files attachable in one chat context. Default: 10. */
473
476
  maxFileCount?: number;
474
- /** Maximum total size in bytes for attached files. Default: 50 * 1024 * 1024 (50 MB). */
477
+ /** Maximum total size in bytes for attached files; `Infinity` for none. Default: 50 * 1024 * 1024 (50 MB). */
475
478
  maxTotalSize?: number;
476
479
  /** Maximum size in bytes of one attached file. Default: none (only `maxTotalSize`). */
477
480
  maxFileSize?: number;
@@ -615,11 +618,21 @@ interface ChatPanelProps {
615
618
  * is a new chat. Left `undefined`, the panel remembers its own.
616
619
  */
617
620
  conversationId?: string | null;
618
- /** Called whenever the conversation on screen changes, whoever changed it. */
619
- onConversationChange?: (conversationId: string | null) => void;
621
+ /**
622
+ * Called whenever the conversation on screen changes, whoever changed it.
623
+ * `created` is true for a conversation the panel has just created from a new
624
+ * chat (its first message or file), false for one opened or left (the list, a
625
+ * reload, a new chat) and for one the backend put in place of a deleted one
626
+ * that was opened.
627
+ */
628
+ onConversationChange?: (conversationId: string | null, change: ChatConversationChange) => void;
620
629
  /** Agent selected once the list loads, by id or slug, over the remembered one. */
621
630
  initialAgent?: string;
622
- /** Called when another agent is picked. */
631
+ /**
632
+ * Called when another agent is picked, in the menu or by opening a
633
+ * conversation of another agent (the picker follows the conversation on
634
+ * screen, so a new chat starts with its agent).
635
+ */
623
636
  onAgentChange?: (agent: XtmAgent) => void;
624
637
  /** Hides the composer: a conversation the user may read but not write to. */
625
638
  readOnly?: boolean;
@@ -682,6 +695,18 @@ interface ChatPanelProps {
682
695
  welcome?: React.ReactNode;
683
696
  /** Rendered under a message's content, before its footer; `context.streaming` while the answer is still coming. */
684
697
  renderMessageExtras?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;
698
+ /**
699
+ * Draws a message in its own way: `undefined` (or a boolean, as
700
+ * `isCut(m) && <Card />` gives) leaves it to the panel, `null` leaves it
701
+ * out, and anything else is drawn in its place, without the message's frame
702
+ * (its name, time and actions). For a host that folds several messages into
703
+ * one of its own (a turn cut and resumed, drawn as one card on the first of
704
+ * them, the others left out). Asked of every message, streaming ones
705
+ * included, the reply's empty placeholder too (`null` leaves out its status
706
+ * bubble): return `undefined` fast for the others. A throw, while asked or
707
+ * while the replacement is drawn, leaves that message to the panel.
708
+ */
709
+ replaceMessage?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;
685
710
  /** Controls added to a finished message's footer. */
686
711
  messageFooterActions?: (message: ChatMessage, context: ChatMessageSlotContext) => React.ReactNode;
687
712
  /** Controls added to a conversation's row in the conversation list. */
@@ -805,6 +830,26 @@ interface ChatPanelProps {
805
830
  * `sidebar*` slots render nothing meanwhile. Default: false.
806
831
  */
807
832
  hideConversationList?: boolean;
833
+ /**
834
+ * The conversation list starts collapsed to its strip, from which the user
835
+ * opens it: where it would squeeze the thread, a phone or a narrow window.
836
+ * Read once, when the panel mounts. Default: false.
837
+ */
838
+ initialConversationListCollapsed?: boolean;
839
+ /**
840
+ * The workspace groups of the conversation list the user has opened (their
841
+ * keys: a workspace's id), for a host that keeps them per user. Given, every
842
+ * other group starts collapsed until the user opens it, so many workspaces
843
+ * do not push the conversations in none below the fold; one the user creates
844
+ * from the list opens. Read once, the first time it is given: a host still
845
+ * reading them passes `undefined` until it has them. The panel keeps what
846
+ * the user opens from then on, while the list is hidden and shown again too.
847
+ * Unset, groups start open and the ones the user collapses are remembered
848
+ * per browser.
849
+ */
850
+ expandedWorkspaceGroups?: string[];
851
+ /** The workspace groups the user has opened, each time they open or collapse one (see `expandedWorkspaceGroups`). */
852
+ onExpandedWorkspaceGroupsChange?: (keys: string[]) => void;
808
853
  /**
809
854
  * Asked before a conversation is filed into a workspace (`null`: taken out
810
855
  * of its workspace), by a drag or the row's Move menu: `false` (or a promise
@@ -826,14 +871,18 @@ interface ChatPanelProps {
826
871
  sidebarSearchThreshold?: number;
827
872
  /**
828
873
  * Names the channel a conversation was started in, on its row (`Started in
829
- * {channel}`): a string names it, `null` shows nothing, `undefined` keeps
874
+ * {channel}`) and in the header (`Agent · {channel}`): a string names it, `null` shows nothing, `undefined` keeps
830
875
  * the panel's own name (Slack, Teams, WhatsApp, Telegram, Discord, Google
831
876
  * Chat; no other channel is shown).
832
877
  */
833
878
  channelLabel?: (channel: string) => string | null | undefined;
834
879
  /** A title beside the agent's name in the header (the conversation's, say), or a function of what is on screen. */
835
880
  headerTitle?: React.ReactNode | ((context: ChatHeaderContext) => React.ReactNode);
836
- /** A line under the agent's name in the header (`Agent · Slack`, say), or a function of what is on screen. */
881
+ /**
882
+ * A line under the agent's name in the header, or a function of what is on
883
+ * screen. Undefined: `Agent · {channel}` for a conversation started in a
884
+ * channel its row names (see `channelLabel`), nothing otherwise.
885
+ */
837
886
  headerSubtitle?: React.ReactNode | ((context: ChatHeaderContext) => React.ReactNode);
838
887
  /**
839
888
  * Links the header's agent name to this address (an http(s) URL or a path
@@ -877,6 +926,11 @@ interface ChatMarkerRenderer {
877
926
  renderWhileStreaming?: boolean;
878
927
  /** Give it the thread's full width rather than the prose's. */
879
928
  fullWidth?: boolean;
929
+ /**
930
+ * An answer that is this marker and nothing else shows the card alone, as a
931
+ * note between messages: no agent name, time or actions around it.
932
+ */
933
+ standalone?: boolean;
880
934
  }
881
935
  /** What a code block renderer is drawn from. */
882
936
  interface ChatCodeBlockContext {
@@ -906,6 +960,11 @@ interface ChatStreamEventInfo {
906
960
  /** The frame acknowledges a turn replayed from its start (`thinking` with `resumed`). */
907
961
  replayed?: boolean;
908
962
  }
963
+ /** Why the conversation on screen changed (see `onConversationChange`). */
964
+ interface ChatConversationChange {
965
+ /** The panel has just created this conversation from a new chat; false when it was opened, replaced or left. */
966
+ created: boolean;
967
+ }
909
968
  /** What a host can do to a mounted panel, through its `ref`. */
910
969
  interface ChatPanelHandle {
911
970
  /** Send *text* as the user, leaving the composer's own draft alone; resolves to what became of it. */
@@ -925,6 +984,12 @@ interface ChatPanelHandle {
925
984
  */
926
985
  getDraft: () => string;
927
986
  focusComposer: () => void;
987
+ /**
988
+ * Put *files* in the composer's tray as if the user picked them (uploaded and
989
+ * checked the same way), to go with the next message: files another app
990
+ * shared to the host. Nothing where the panel takes no files.
991
+ */
992
+ attachFiles: (files: readonly File[]) => void;
928
993
  /** Same as the Stop button; also cancels the question a voice call awaits, and stops it speaking. */
929
994
  stop: () => void;
930
995
  /**
@@ -1349,6 +1414,12 @@ interface XtmAgent {
1349
1414
  group?: string;
1350
1415
  /** Short labels shown after its name in the agent picker (`Company`), already translated. */
1351
1416
  badges?: string[];
1417
+ /**
1418
+ * The agent is meant for assignments rather than chat (`disable_chat`): the
1419
+ * picker says "Not recommended for chat" and the composer warns while it is
1420
+ * the one written to. It stays selectable.
1421
+ */
1422
+ disableChat?: boolean;
1352
1423
  }
1353
1424
 
1354
1425
  /**
@@ -1369,4 +1440,4 @@ interface TransferredAgent {
1369
1440
  declare const voiceCallSupported: () => boolean;
1370
1441
 
1371
1442
  export { ChatPanel, ChatToggleButton, voiceCallSupported };
1372
- export type { AgentStatusState, ApiEndpoints, BackendType, ChatAnsweredBy, ChatArtifact, ChatArtifactKind, ChatAttachment, ChatCodeBlockContext, ChatCodeBlockRenderer, ChatContextBreakdown, ChatContextUsage, ChatConversationAction, ChatConversationRef, ChatConversationReferenceCandidate, ChatConversationSummary, ChatFile, ChatFileRefusal, ChatHeaderContext, ChatHeldOutcome, ChatMarkerContext, ChatMarkerRenderer, ChatMessage, ChatMessagePersistedFeedback, ChatMessageSlotContext, ChatMode, ChatPanelHandle, ChatPanelMode, ChatPanelProps, ChatPanelTheme, ChatPromptTemplate, ChatQuotaStatus, ChatSendDecision, ChatSendDraft, ChatSendOptions, ChatSendOutcome, ChatSidebarSearch, ChatStreamEventInfo, ChatToggleButtonProps, ChatToolLabels, ChatTurnEnd, ChatTurnOutcome, ChatWorkspace, ConsultStatus, MessageFeedback, ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict, TransferredAgent, Translate, XtmAgent };
1443
+ export type { AgentStatusState, ApiEndpoints, BackendType, ChatAnsweredBy, ChatArtifact, ChatArtifactKind, ChatAttachment, ChatCodeBlockContext, ChatCodeBlockRenderer, ChatContextBreakdown, ChatContextUsage, ChatConversationAction, ChatConversationChange, ChatConversationRef, ChatConversationReferenceCandidate, ChatConversationSummary, ChatFile, ChatFileRefusal, ChatHeaderContext, ChatHeldOutcome, ChatMarkerContext, ChatMarkerRenderer, ChatMessage, ChatMessagePersistedFeedback, ChatMessageSlotContext, ChatMode, ChatPanelHandle, ChatPanelMode, ChatPanelProps, ChatPanelTheme, ChatPromptTemplate, ChatQuotaStatus, ChatSendDecision, ChatSendDraft, ChatSendOptions, ChatSendOutcome, ChatSidebarSearch, ChatStreamEventInfo, ChatToggleButtonProps, ChatToolLabels, ChatTurnEnd, ChatTurnOutcome, ChatWorkspace, ConsultStatus, MessageFeedback, ToolApprovalDecision, ToolApprovalProposal, ToolApprovalVerdict, TransferredAgent, Translate, XtmAgent };